package bancosys.tec.utils.db.executor;

import java.io.IOException;
import java.net.URL;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import jmine.tec.utils.UtilsMessages;
import bancosys.tec.utils.db.CouldNotProcessStatementException;
import bancosys.tec.utils.db.StatementSplitter;
import bancosys.tec.utils.reader.URLReader;
import bancosys.tec.utils.reader.URLReaderFactory;

/**
 * @author Marcio Ribeiro (mmr)
 * @created Apr 26, 2007
 */
public abstract class JdbcDbExecutor extends AbstractDbExecutor {
    private static final String NO_UPPER_CASE = "--NoUpperCase\n";

    /**
     * Template de método para execução do script.
     * 
     * @param dropBefore apaga todas as tableas quando <code>true</code>, não apaga quando <code>false</code>.
     * @throws CouldNotExecuteException quando nao consegue executar o script.
     */
    public void execute(boolean dropBefore) throws CouldNotExecuteException {
        try {
            final Connection conn = this.getConnection();

            if (dropBefore) {
                this.executeDrop(conn);
            }

            // Executando todos arquivos, recursivamente
            this.executeFiles(conn);
        } catch (SQLException e) {
            throw new CouldNotExecuteException(UtilsMessages.SQLEXCEPTION.create(), e);
        } catch (IOException e) {
            throw new CouldNotExecuteException(UtilsMessages.IOEXCEPTION.create(), e);
        } catch (CouldNotProcessStatementException e) {
            throw new CouldNotExecuteException(UtilsMessages.COULD_NOT_PROCESS_STATEMENT.create(e.getMessage()), e);
        }
    }

    /**
     * Executa o statement no statement.
     * 
     * @param statement statement
     * @throws SQLException SQLException
     */
    public void executeSingleStatement(String statement) throws SQLException {
        if (statement != null && statement.length() > 0) {
            String st = statement.replaceAll(this.getDelimiterChar() + "\\s*$", "");
            AbstractDbExecutor.LOG.debug("Query: " + st);
            String str = JdbcDbExecutor.this.convertStatement(this.doUpperCase(st));
            PreparedStatement sqlSt = JdbcDbExecutor.this.createStatement(str);
            sqlSt.executeUpdate();
            sqlSt.close();
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @throws CouldNotExecuteException
     */
    public void drop() throws CouldNotExecuteException {
        Connection connection;
        try {
            connection = this.getConnection();
        } catch (SQLException e) {
            throw new CouldNotExecuteException(UtilsMessages.SQLEXCEPTION.create(), e);
        }
        this.executeDrop(connection);
    }

    /**
     * Executa o drop no bd dada a conexão.
     * 
     * @param conn Connection
     * @throws CouldNotExecuteException Se não for possível executar o drop
     */
    protected void executeDrop(Connection conn) throws CouldNotExecuteException {
        // Para ser sobrecarregado especificamente para cada banco de dados
    }

    /**
     * Execute os arquivos nesta conexão.
     * 
     * @param conn a COnexão.
     * @throws IOException Se não for possível ler um arquivo
     * @throws CouldNotProcessStatementException Se não for possível processar um statement
     * @throws SQLException Se não for possível executar um comando SQL
     */
    protected void executeFiles(final Connection conn) throws IOException, CouldNotProcessStatementException, SQLException {
        // Pega todos arquivos que casam com o padrao, recursivamente
        List<URL> filesToExecute = this.getAllFilesToExecute(this.getFile(), this.getScriptPattern());

        // Ordena
        Collections.sort(filesToExecute, new Comparator<URL>() {

            public int compare(URL o1, URL o2) {
                return o1.getPath().compareTo(o2.getPath());
            }
        });

        // Executa os arquivos que casaram com o padrao
        for (URL file : filesToExecute) {
            AbstractDbExecutor.LOG.debug("File: " + file.getPath());
            new DbExecutorStatementSplitter(file, this.getDelimiterChar()).split();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        }
    }

    /**
     * Devolve arquivos no diretório passado (recursivamente) que casam com o padrão passado.
     * 
     * @param root diretório base.
     * @param pattern padrão de nomenclatura dos arquivos.
     * @return coleção de arquivos.
     * @throws CouldNotProcessStatementException Se não for possível processar um statement
     */
    private List<URL> getAllFilesToExecute(URL root, String pattern) throws CouldNotProcessStatementException {

        URLReader scriptReader = URLReaderFactory.getFileReader(root);
        try {
            return scriptReader.getFiles(root, pattern);
        } catch (Exception e) {
            throw new CouldNotProcessStatementException(UtilsMessages.ERROR_FINDING_FILES.create(), e);
        }
    }

    /**
     */
    class DbExecutorStatementSplitter extends StatementSplitter {

        /**
         * C'tor
         * 
         * @param file o arquivo que deve ser executado
         * @param delimiterChar o char que delimita statements
         */
        public DbExecutorStatementSplitter(URL file, char delimiterChar) {
            super(file, delimiterChar);
        }

        /**
         * processa um statement
         * 
         * @param st um statement
         * @throws CouldNotProcessStatementException Se não for possível processar o statement
         */
        @Override
        protected void processStatement(String st) throws CouldNotProcessStatementException {
            try {
                JdbcDbExecutor.this.executeSingleStatement(st);
            } catch (Throwable e) {
                throw new CouldNotProcessStatementException(UtilsMessages.ERROR_PROCESSING_STATEMENT.create(this.file.getPath(),
                        this.lineNumber, st), e);
            }
        }
    };

    /**
     * Devolve o delimitador para os statements.
     * 
     * @return caractere delimitador para os statements.
     */
    public abstract char getDelimiterChar();

    /**
     * Prepares a statement with the given str as the sql command statement.
     * 
     * @param str str
     * @return PreparedStatement
     * @throws SQLException SQLException
     */
    protected PreparedStatement createStatement(String str) throws SQLException {
        return this.getConnection().prepareStatement(str);
    }

    /**
     * Hook para converter um statement especifico
     * 
     * @param statement o statement
     * @return String
     */
    public String convertStatement(String statement) {
        return statement;
    }

    /**
     * Retorna a string em uppercase, caso seja necessário. Caso a string comece com "--NoUpperCase\n", o uppercase nunca é feito, e a
     * string é retornada com a expressão removida.
     * 
     * @param str str
     * @return str
     */
    private String doUpperCase(String str) {
        boolean blockUpperCase = str.startsWith(NO_UPPER_CASE);
        if (blockUpperCase) {
            return str.substring(NO_UPPER_CASE.length());
        } else if (JdbcDbExecutor.this.getToUpper()) {
            return str.toUpperCase();
        }
        return str;
    }
}